using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;

namespace WikiUploader.Classes.DotNetWikiBot
{
   /// <summary>Class defines a WikiBot instance, some configuration settings
   /// and some auxiliary functions.</summary>
   [ClassInterface(ClassInterfaceType.AutoDispatch)]
   public class WikiBot
   {
      /// <summary>Title and description of web agent.</summary>
      public static readonly string BotVer = "DotNetWikiBot";

      /// <summary>Version of DotNetWikiBot Framework.</summary>
      public static readonly Version Version = new Version("2.98");

      /// <summary>Desired bot's messages language (ISO 639-1 language code).
      /// If not set explicitly, the language will be detected automatically.</summary>
      /// <example><code>WikiBot.botMessagesLang = "fr";</code></example>
      private static readonly string BotMessagesLang;

      /// <summary>Default edit comment. You can set it to what you like.</summary>
      /// <example><code>WikiBot.editComment = "My default edit comment";</code></example>
      public static string EditComment { get; set; }

      /// <summary>If set to true, all the bot's edits are marked as minor by default.</summary>
      public static bool IsMinorEdit { get; set; }

      /// <summary>If true, the bot uses "MediaWiki API" extension
      /// (special MediaWiki bot interface, "api.php"), if it is available.
      /// If false, the bot uses common user interface. True by default.
      /// Set it to false manually, if some problem with bot interface arises on site.</summary>
      /// <example><code>WikiBot.useBotQuery = false;</code></example>
      public static bool UseBotQuery { get; set; }

      /// <summary>Number of times to retry bot action in case of temporary connection failure or
      /// some other common net problems.</summary>
      public static int RetryTimes { get; set; }

      /// <summary>If true, the bot asks user to confirm next Save, RenameTo or Delete operation.
      /// False by default. Set it to true manually, when necessary.</summary>
      /// <example><code>WikiBot.askConfirm = true;</code></example>
      public static bool AskConfirm { get; set; }

      /// <summary>If true, bot only reports errors and warnings. Call EnableSilenceMode
      /// function to enable that mode, don't change this variable's value manually.</summary>
      private static bool s_SilenceMode;

      /// <summary>If set to some file name (e.g. "DotNetWikiBot_Report.txt"), the bot
      /// writes all output to that file instead of a console. If no path was specified,
      /// the bot creates that file in it's current directory. File is encoded in UTF-8.
      /// Call EnableLogging function to enable log writing, don't change this variable's
      /// value manually.</summary>
      private static string s_LogFile;

      /// <summary>Array, containing localized DotNetWikiBot interface messages.</summary>
      private static readonly SortedDictionary<string, string> Messages =
         new SortedDictionary<string, string>();

      /// <summary>Internal web client, that is used to access sites.</summary>
      public static WebClient WebClient { get; set; }

      /// <summary>Content type for HTTP header of web client.</summary>
      public static string WebContentType { get { return "application/x-www-form-urlencoded"; } }

      /// <summary>If true, assembly is running on Mono framework. If false,
      /// it is running on original Microsoft .NET Framework. This variable is set
      /// automatically.</summary>
      public static bool IsRunningOnMono { get { return Type.GetType ("Mono.Runtime") != null; } }

      /// <summary>Initial state of HttpWebRequestElement.UseUnsafeHeaderParsing boolean
      /// configuration setting. 0 means true, 1 means false, 2 means unchanged.</summary>
      public static int UnsafeHttpHeaderParsingUsed { get; set; }

      /// <summary>This constructor is used to generate WikiBot object.</summary>
      /// <returns>Returns WikiBot object.</returns>
      static WikiBot()
      {
         RetryTimes = 3;
         UseBotQuery = true;
         IsMinorEdit = true;
         WebClient = new WebClient ();
         UnsafeHttpHeaderParsingUsed = 2;
         EditComment = "Automatic page editing";
         if (BotMessagesLang == null)
            BotMessagesLang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
         if (BotMessagesLang != "en")
            if (!LoadLocalizedMessages (BotMessagesLang))
               BotMessagesLang = "en";
         BotVer += "/" + Version + " (" + Environment.OSVersion.VersionString + "; " +
                   ".NET CLR " + Environment.Version + ")";
         ServicePointManager.Expect100Continue = false;
      }

      /*/// <summary>The destructor is used to uninitialize WikiBot objects.</summary>
		~WikiBot()
		{
			//if (unsafeHttpHeaderParsingUsed != 2)
				//SwitchUnsafeHttpHeaderParsing(unsafeHttpHeaderParsingUsed == 1 ? true : false);
		}*/

      /// <summary>Call this function to make bot write all output to the specified file
      /// instead of a console. If only error logging is desirable, first call this
      /// function, and after that call EnableSilenceMode function.</summary>
      /// <param name="logFileName">Path and name of a file to write output to.
      /// If no path was specified, the bot creates that file in it's current directory.
      /// File is encoded in UTF-8.</param>
      public static void EnableLogging(string logFileName)
      {
         s_LogFile = logFileName;
         StreamWriter log = File.AppendText(s_LogFile);
         log.AutoFlush = true;
         Console.SetError(log);
         if (!s_SilenceMode)
            Console.SetOut(log);
      }

      /// <summary>Call this function to make bot report only errors and warnings,
      /// no other messages will be displayed or logged.</summary>
      public static void EnableSilenceMode()
      {
         s_SilenceMode = true;
         Console.SetOut(new StringWriter());
      }

      /// <summary>Function loads localized bot interface messages from 
      /// "DotNetWikiBot.i18n.xml" file. Function is called in WikiBot class constructor, 
      /// but can also be called manually to change interface language at runtime.</summary>
      /// <param name="language">Desired language's ISO 639-1 code.</param>
      /// <returns>Returns false, if messages for specified language were not found.
      /// Returns true on success.</returns>
      public static bool LoadLocalizedMessages(string language)
      {
         if (!File.Exists("DotNetWikiBot.i18n.xml")) {
            Console.Error.WriteLine("Localization file \"DotNetWikiBot.i18n.xml\" is missing.");
            return false;
         }
         using (XmlReader reader = XmlReader.Create("DotNetWikiBot.i18n.xml")) {
            if (!reader.ReadToFollowing(language)) {
               Console.Error.WriteLine("\nLocalized messages not found for language \"{0}\"." +
                                       "\nYou can help DotNetWikiBot project by translating the messages in\n" +
                                       "\"DotNetWikiBot.i18n.xml\" file and sending it to developers for " +
                                       "distribution.\n", language);
               return false;
            }
            if (!reader.ReadToDescendant("msg"))
               return false;
            if (Messages.Count > 0)
               Messages.Clear();
            var id = reader ["id"];
            if (!string.IsNullOrEmpty (id))
            {
               if (Messages.ContainsKey (id)) Messages[id] = reader.ReadString ();
               while (reader.ReadToNextSibling ("msg"))
               {
                  id = reader["id"];
                  if (!string.IsNullOrEmpty (id))
                     Messages [id] = reader.ReadString ();
               }
            }
         }
         return true;
      }

      /// <summary>The function gets localized (translated) form of the specified bot
      /// interface message.</summary>
      /// <param name="message">Message itself, placeholders for substituted parameters are
      /// denoted in curly brackets like {0}, {1}, {2} and so on.</param>
      /// <returns>Returns localized form of the specified bot interface message,
      /// or English form if localized form was not found.</returns>
      public static string Msg(string message)
      {
         if (BotMessagesLang == "en")
            return message;
         try {
            return Messages[message];
         }
         catch (KeyNotFoundException) {
            return message;
         }
      }

      /// <summary>This function asks user to confirm next action. The message
      /// "Would you like to proceed (y/n/a)? " is displayed and user response is
      /// evaluated. Make sure to set "askConfirm" variable to "true" before
      /// calling this function.</summary>
      /// <returns>Returns true, if user has confirmed the action.</returns>
      /// <example><code>
      /// if (WikiBot.askConfirm) {
      ///     Console.Write("Some action on live wiki is going to occur.\n\n");
      ///     if(!WikiBot.UserConfirms())
      ///         return;
      /// }
      /// </code></example>
      public static bool UserConfirms()
      {
         if (!AskConfirm)
            return true;
         Console.Write(Msg("Would you like to proceed (y/n/a)?") + " ");
         ConsoleKeyInfo k = Console.ReadKey();
         Console.Write("\n");
         if (k.KeyChar == 'y')
            return true;
         if (k.KeyChar == 'a') {
            AskConfirm = false;
            return true;
         }
         return false;
      }

      /// <summary>This auxiliary function counts the occurrences of specified string
      /// in specified text. This count is often needed, but strangely there is no
      /// such function in .NET Framework's String class.</summary>
      /// <param name="text">String to look in.</param>
      /// <param name="str">String to look for.</param>
      /// <param name="ignoreCase">Pass "true" if you need case-insensitive search.
      /// But remember that case-sensitive search is faster.</param>
      /// <returns>Returns the number of found occurrences.</returns>
      /// <example><code>int m = CountMatches("Bot Bot bot", "Bot", false); // =2</code></example>
      public static int CountMatches(string text, string str, bool ignoreCase)
      {
         if (string.IsNullOrEmpty(text))
            throw new ArgumentNullException("text");
         if (string.IsNullOrEmpty(str))
            throw new ArgumentNullException("str");
         int matches = 0;
         int position = 0;
         StringComparison rule = ignoreCase
                                    ? StringComparison.OrdinalIgnoreCase
                                    : StringComparison.Ordinal;
         while ((position = text.IndexOf(str, position, rule)) != -1) {
            matches++;
            position++;
         }
         return matches;
      }

      /// <summary>This auxiliary function returns the zero-based indexes of all occurrences
      /// of specified string in specified text.</summary>
      /// <param name="text">String to look in.</param>
      /// <param name="str">String to look for.</param>
      /// <param name="ignoreCase">Pass "true" if you need case-insensitive search.
      /// But remember that case-sensitive search is faster.</param>
      /// <returns>Returns the List of positions (zero-based integer indexes) of all found
      /// instances, or empty List if nothing was found.</returns>
      public static List<int> GetMatchesPositions(string text, string str, bool ignoreCase)
      {
         if (string.IsNullOrEmpty(text))
            throw new ArgumentNullException("text");
         if (string.IsNullOrEmpty(str))
            throw new ArgumentNullException("str");
         var positions = new List<int>();
         StringComparison rule = ignoreCase
                                    ? StringComparison.OrdinalIgnoreCase
                                    : StringComparison.Ordinal;
         int position = 0;
         while ((position = text.IndexOf(str, position, rule)) != -1) {
            positions.Add(position);
            position++;
         }
         return positions;
      }

      /// <summary>This auxiliary function makes the first letter in specified string upper-case.
      /// This is often needed, but strangely there is no such function in .NET Framework's
      /// String class.</summary>
      /// <param name="str">String to capitalize.</param>
      /// <returns>Returns capitalized string.</returns>
      public static string Capitalize(string str)
      {
         return char.ToUpper(str[0]) + str.Substring(1);
      }

      /// <summary>This auxiliary function makes the first letter in specified string lower-case.
      /// This is often needed, but strangely there is no such function in .NET Framework's
      /// String class.</summary>
      /// <param name="str">String to uncapitalize.</param>
      /// <returns>Returns uncapitalized string.</returns>
      public static string Uncapitalize(string str)
      {
         return char.ToLower(str[0]) + str.Substring(1);
      }

      /// <summary>Suspends execution for specified number of seconds.</summary>
      /// <param name="seconds">Number of seconds to wait.</param>
      public static void Wait(int seconds)
      {
         Thread.Sleep(seconds * 1000);
      }

      /// <summary>This internal function switches unsafe HTTP headers parsing on or off.
      /// This is needed to ignore unimportant HTTP protocol violations,
      /// committed by misconfigured web servers.</summary>
      public static void SwitchUnsafeHttpHeaderParsing(bool enabled)
      {
         System.Configuration.Configuration config =
            System.Configuration.ConfigurationManager.OpenExeConfiguration(
               System.Configuration.ConfigurationUserLevel.None);
         var section = (System.Net.Configuration.SettingsSection)config.GetSection("system.net/settings");
         if (UnsafeHttpHeaderParsingUsed == 2)
            UnsafeHttpHeaderParsingUsed = section.HttpWebRequest.UseUnsafeHeaderParsing ? 1 : 0;
         section.HttpWebRequest.UseUnsafeHeaderParsing = enabled;
         config.Save();
         System.Configuration.ConfigurationManager.RefreshSection("system.net/settings");
      }

      /// <summary>This internal function clears the CanonicalizeAsFilePath attribute in
      /// .NET UriParser to fix a major .NET bug when System.Uri incorrectly strips trailing 
      /// dots in URIs. The bug was discussed in details at:
      /// https://connect.microsoft.com/VisualStudio/feedback/details/386695/system-uri-in
      /// </summary>
      public static void DisableCanonicalizingUriAsFilePath()
      {
         MethodInfo getSyntax = typeof(UriParser).GetMethod("GetSyntax", BindingFlags.Static | BindingFlags.NonPublic);
         FieldInfo flagsField = typeof(UriParser).GetField("m_Flags", BindingFlags.Instance | BindingFlags.NonPublic);
         if (getSyntax != null && flagsField != null)
         {
            foreach (string scheme in new [] { "http", "https" })
            {
               var parser = (UriParser)getSyntax.Invoke(null, new object[] { scheme });
               if (parser != null)
               {
                  var flagsValue = (int)flagsField.GetValue(parser);
                  // Clear the CanonicalizeAsFilePath attribute
                  if ((flagsValue & 0x1000000) != 0)
                     flagsField.SetValue(parser, flagsValue & ~0x1000000);
               }
            }
         }
      }

      /// <summary>This internal function removes all attributes from root XML/XHTML element
      /// (XML namespace declarations, schema links, etc.) for easy processing.</summary>
      /// <returns>Returns document without unnecessary declarations.</returns>
      public static string RemoveXmlRootAttributes(string xmlSource)
      {
         int startPos = ((xmlSource.StartsWith("<!") || xmlSource.StartsWith("<?"))
                         && xmlSource.IndexOf('>') != -1) ? xmlSource.IndexOf('>') + 1 : 0;
         int firstSpacePos = xmlSource.IndexOf(' ', startPos);
         int firstCloseTagPos = xmlSource.IndexOf('>', startPos);
         if (firstSpacePos != -1 && firstCloseTagPos != -1 && firstSpacePos < firstCloseTagPos)
            return xmlSource.Remove(firstSpacePos, firstCloseTagPos - firstSpacePos);
         return xmlSource;
      }

      /// <summary>This internal function initializes web client to get resources
      /// from web.</summary>
      public static void InitWebClient()
      {
         if (!IsRunningOnMono)
            WebClient.UseDefaultCredentials = true;
         WebClient.Encoding = Encoding.UTF8;
         WebClient.Headers.Add("Content-Type", WebContentType);
         WebClient.Headers.Add("User-agent", BotVer);
      }

      /// <summary>This internal wrapper function gets web resource in a fault-tolerant manner.
      /// It should be used only in simple cases, because it sends no cookies, it doesn't support
      /// traffic compression and lacks other special features.</summary>
      /// <param name="address">Web resource address.</param>
      /// <param name="postData">Data to post with web request, can be "" or null.</param>
      /// <returns>Returns web resource as text.</returns>
      public static string GetWebResource(Uri address, string postData)
      {
         string webResourceText;
         for (int errorCounter = 0; ; errorCounter++) {
            try {
               InitWebClient();
               webResourceText = string.IsNullOrEmpty(postData) ? WebClient.DownloadString(address) : WebClient.UploadString(address, postData);
               break;
            }
            catch (WebException e) {
               if (errorCounter > RetryTimes)
                  throw;
               string message = e.Message;
               if (Regex.IsMatch(message, ": \\(50[02349]\\) ")) {		// Remote problem
                  Console.Error.WriteLine(message + " " + Msg("Retrying in 60 seconds."));
                  Thread.Sleep(60000);
               }
               else if (message.Contains("Section=ResponseStatusLine")) {	// Squid problem
                  SwitchUnsafeHttpHeaderParsing(true);
                  Console.Error.WriteLine(message + " " + Msg("Retrying in 60 seconds."));
                  Thread.Sleep(60000);
               }
               else
                  throw;
            }
         }
         return webResourceText;
      }
   }
}